using System;
using System.Reflection;
using Velocity4Net.Util.Introspection;
using Velocity4Net.Ctx;
using Velocity4Net.Util;
using Velocity4Net.Errors;
using Velocity4Net.Runtime.Directives;
using Velocity4Net.App.Events;

namespace Velocity4Net.Runtime.Parse.AST
{
    /**
     *  ASTMethod.java
     *
     *  Method support for references :  $foo.method()
     *
     *  NOTE :
     *
     *  introspection is now done at render time.
     *
     *  Please look at the Parser.jjt file which is
     *  what controls the generation of this class.
     *
     * @author <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
     * @author <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
     * @version $Id: ASTMethod.java 898032 2010-01-11 19:51:03Z nbubna $
     */
    public class ASTMethod : SimpleNode
    {
        private String methodName = "";
        private int paramCount = 0;

        protected Info uberInfo;

        /**
         * Indicates if we are running in strict reference mode.
         */
        protected bool strictRef = false;

        /**
         * @param id
         */
        public ASTMethod(int id)
            : base(id)
        {
        }

        /**
         * @param p
         * @param id
         */
        public ASTMethod(Parser p, int id)
            : base(p, id)
        {
        }

        /**
         *  simple init - init our subtree and get what we can from
         *  the AST
         * @param context
         * @param data
         * @return The init result
         * @throws TemplateInitException
         */
        public override Object Init(InternalContextAdapter context, Object data)
        {
            base.Init(context, data);

            /*
             * make an uberinfo - saves new's later on
             */

            uberInfo = new Info(TemplateName,
                    Line, Column);
            /*
             *  this is about all we can do
             */

            methodName = FirstToken.image;
            paramCount = jjtGetNumChildren() - 1;

            strictRef = rsvc.GetBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT, false);

            return data;
        }

        /**
         *  invokes the method.  Returns null if a problem, the
         *  actual return if the method returns something, or
         *  an empty string "" if the method returns void
         * @param o
         * @param context
         * @return Result or null.
         * @throws MethodInvocationException
         */
        public override Object Execute(Object o, InternalContextAdapter context)
        {
            /*
             *  new strategy (strategery!) for introspection. Since we want
             *  to be thread- as well as context-safe, we *must* do it now,
             *  at execution time.  There can be no in-node caching,
             *  but if we are careful, we can do it in the context.
             */
            Object[] _params = new Object[paramCount];

            /*
             * sadly, we do need recalc the values of the args, as this can
             * change from visit to visit
             */
            Type[] paramClasses =
               paramCount > 0 ? new Type[paramCount] : new Type[0];

            for (int j = 0; j < paramCount; j++)
            {
                _params[j] = jjtGetChild(j + 1).GetValue(context);
                if (_params[j] != null)
                {
                    paramClasses[j] = _params[j].GetType();
                }
            }

            VelMethod method = ClassUtils.getMethod(methodName, _params, paramClasses,
                o, context, this, strictRef);
            if (method == null) return null;

            try
            {
                /*
                 *  get the returned object.  It may be null, and that is
                 *  valid for something declared with a void return type.
                 *  Since the caller is expecting something to be returned,
                 *  as long as things are peachy, we can return an empty
                 *  String so ASTReference() correctly figures out that
                 *  all is well.
                 */

                Object obj = method.invoke(o, _params);

                if (obj == null)
                {
                    if (method.getReturnType().Name == "Void")
                    {
                        return "";
                    }
                }

                return obj;
            }
            catch (TargetInvocationException ite)
            {
                return handleInvocationException(o, context, ite.InnerException);
            }

            /** Can also be thrown by method invocation **/
            catch (ArgumentException t)
            {
                return handleInvocationException(o, context, t);
            }

            /**
             * pass through application level runtime exceptions
             */
            catch (ApplicationException e)
            {
                throw e;
            }
            catch (System.Exception e)
            {
                String msg = "ASTMethod.execute() : exception invoking method '"
                             + methodName + "' in " + o.GetType();
                log.error(msg, e);
                throw new VelocityException(msg, e);
            }
        }

        private Object handleInvocationException(Object o, InternalContextAdapter context, System.Exception t)
        {
            /*
             * We let StopCommands go up to the directive they are for/from
             */
            if (t is StopCommand)
            {
                throw (StopCommand)t;
            }

            /*
             *  In the event that the invocation of the method
             *  itself throws an exception, we want to catch that
             *  wrap it, and throw.  We don't log here as we want to figure
             *  out which reference threw the exception, so do that
             *  above
             */
            else if (t is System.Exception)
            {
                try
                {
                    return EventHandlerUtil.methodException(rsvc, context, o.GetType(), methodName, (System.Exception)t);
                }

                /**
                 * If the event handler throws an exception, then wrap it
                 * in a MethodInvocationException.  Don't pass through RuntimeExceptions like other
                 * similar catchall code blocks.
                 */
                catch (System.Exception e)
                {
                    throw new MethodInvocationException(
                        "Invocation of method '"
                        + methodName + "' in  " + o.GetType()
                        + " threw exception "
                        + e.ToString(),
                        e, methodName, TemplateName, this.Line, this.Column);
                }
            }

            /*
             *  let non-Exception Throwables go...
             */
            else
            {
                /*
                 * no event cartridge to override. Just throw
                 */

                throw new MethodInvocationException(
                "Invocation of method '"
                + methodName + "' in  " + o.GetType()
                + " threw exception "
                + t.ToString(),
                t, methodName, TemplateName, this.Line, this.Column);
            }
        }

        /**
         * Internal class used as key for method cache.  Combines
         * ASTMethod fields with array of parameter classes.  Has
         * public access (and complete constructor) for unit test 
         * purposes.
         * @since 1.5
         */
        public class MethodCacheKey
        {
            private String methodName;
            private Type[] _params;

            public MethodCacheKey(String methodName, Type[] _params)
            {
                /** 
                 * Should never be initialized with nulls, but to be safe we refuse 
                 * to accept them.
                 */
                this.methodName = (methodName != null) ? methodName : string.Empty;
                this._params = (_params != null) ? _params : new Type[] { };
            }

            /**
             * @see java.lang.Object#equals(java.lang.Object)
             */
            public override bool Equals(Object o)
            {
                /** 
                 * note we skip the null test for methodName and params
                 * due to the earlier test in the constructor
                 */
                if (o is MethodCacheKey)
                {
                    MethodCacheKey other = (MethodCacheKey)o;
                    if (_params.Length == other._params.Length &&
                            methodName.Equals(other.methodName))
                    {
                        for (int i = 0; i < _params.Length; ++i)
                        {
                            if (_params[i] == null)
                            {
                                if (_params[i] != other._params[i])
                                {
                                    return false;
                                }
                            }
                            else if (!_params[i].Equals(other._params[i]))
                            {
                                return false;
                            }
                        }
                        return true;
                    }
                }
                return false;
            }


            /**
             * @see java.lang.Object#hashCode()
             */
            public int hashCode()
            {
                int result = 17;

                /** 
                 * note we skip the null test for methodName and params
                 * due to the earlier test in the constructor
                 */
                for (int i = 0; i < _params.Length; ++i)
                {
                    Type param = _params[i];
                    if (param != null)
                    {
                        result = result * 37 + param.GetHashCode();
                    }
                }

                result = result * 37 + methodName.GetHashCode();

                return result;
            }
        }

        /**
         * @return Returns the methodName.
         * @since 1.5
         */
        public String getMethodName()
        {
            return methodName;
        }
    }
}
